/*
	Copyright (C) 2008 Jeffrey Sharkey, http://jsharkey.org/
	
	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.
	
	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.
	
	You should have received a copy of the GNU General Public License
	along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package org.jsharkey.oilcan;

import java.io.InputStream;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Semaphore;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.SearchManager;
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.DialogInterface.OnClickListener;
import android.content.SharedPreferences.Editor;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.view.MenuItem.OnMenuItemClickListener;
import android.webkit.JsResult;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.FrameLayout;
import android.widget.SimpleCursorAdapter;
import android.widget.Toast;

public class BrowserActivity extends Activity {

	public final static String TAG = BrowserActivity.class.toString();

	private WebView webview = null;
	
	private long magickey = new Random().nextLong();
	
	private Semaphore resultWait = new Semaphore(0);
	private int resultCode = Activity.RESULT_CANCELED;
	private Intent resultData = null;
	
	private ScriptDatabase scriptdb = null;
	
	public final static String LAST_VIEWED = "lastviewed";

	public void onCreate(Bundle icicle) {
		super.onCreate(icicle);
		
        requestWindowFeature(Window.FEATURE_PROGRESS);
		setContentView(R.layout.act_browse);
		
		scriptdb = new ScriptDatabase(this);
		scriptdb.onUpgrade(scriptdb.getWritableDatabase(), -10, 10);

		webview = (WebView)findViewById(R.id.browse_webview);

		WebSettings settings = webview.getSettings();
		settings.setSavePassword(false);
		settings.setSaveFormData(false);
		settings.setJavaScriptEnabled(true);
		settings.setSupportZoom(true);
		settings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
		
		FrameLayout zoomholder = (FrameLayout)this.findViewById(R.id.browse_zoom);
		zoomholder.addView(webview.getZoomControls());
		webview.getZoomControls().setVisibility(View.GONE);
		
		webview.setWebViewClient(new OilCanClient());
		webview.setWebChromeClient(new OilCanChrome());
		
		webview.addJavascriptInterface(new IntentHelper(), "intentHelper");
		
		// load the last-viewed page into browser
		String url = "http://m.half.com/";
		if(icicle != null && icicle.containsKey(LAST_VIEWED))
			url = icicle.getString(LAST_VIEWED);

		// or watch for incoming requested urls
		if(getIntent().getExtras() != null && getIntent().getExtras().containsKey(SearchManager.QUERY))
			url = getIntent().getStringExtra(SearchManager.QUERY);
		
		webview.loadUrl(url);
	
	}
	
	private void loadNewPage(String url) {
		// reset blocked flag (when implemented) and load new page
		webview.loadUrl(url);
	}

	public void onNewIntent(Intent intent) {
		// pull new url from query
		String url = intent.getStringExtra(SearchManager.QUERY);
		this.loadNewPage(url);
	}
	
	protected void onSaveInstanceState(Bundle outState) {
		outState.putString(LAST_VIEWED, webview.getUrl());
	}

	public void onDestroy() {
		super.onDestroy();
		this.scriptdb.close();
	}
	
	public boolean onCreateOptionsMenu(Menu menu) {
		super.onCreateOptionsMenu(menu);
		
		MenuItem gourl = menu.add(R.string.browse_gotourl);
		gourl.setIcon(R.drawable.ic_menu_goto);
		gourl.setOnMenuItemClickListener(new OnMenuItemClickListener() {
			public boolean onMenuItemClick(MenuItem item) {
				BrowserActivity.this.startSearch(webview.getUrl(), true, null, false);
				return true;
			}
		});
		
		MenuItem refresh = menu.add(R.string.browse_refresh);
		refresh.setIcon(R.drawable.ic_menu_refresh);
		refresh.setOnMenuItemClickListener(new OnMenuItemClickListener() {
			public boolean onMenuItemClick(MenuItem item) {
				webview.reload();
				return true;
			}
		});

		MenuItem scripts = menu.add(R.string.browse_manage);
		scripts.setIcon(android.R.drawable.ic_menu_agenda);
		scripts.setIntent(new Intent(BrowserActivity.this, ScriptListActivity.class));

		MenuItem example = menu.add(R.string.browse_example);
		example.setIcon(R.drawable.ic_menu_bookmark);
		example.setOnMenuItemClickListener(new OnMenuItemClickListener() {
			public boolean onMenuItemClick(MenuItem item) {
				final String[] examples = BrowserActivity.this.getResources().getStringArray(R.array.list_examples);
				new AlertDialog.Builder(BrowserActivity.this)
					.setTitle(R.string.browse_example_title)
					.setItems(examples, new OnClickListener() {
						public void onClick(DialogInterface dialog, int which) {
							BrowserActivity.this.loadNewPage(examples[which]);
						}
					}).create().show();

				return true;
			}
		});

		return true;
	}
	
	/**
	 * Pass a resulting intent down to the waiting script call.
	 */
	protected void onActivityResult(int requestCode, int resultCode, Intent data) {
		this.resultCode = resultCode;
		this.resultData = data;
		this.resultWait.release();
	}
	
	public final static String MATCH = "intentHelper.startActivity(",
		MATCH_RESULT = "intentHelper.startActivityForResult(";
	
	
	/**
	 * Prepare the given script for execution, specifically by injecting our
	 * magic key for any {@link IntentHelper} calls.
	 */
	private String prepareScript(String script) {
		String jsonlib = "";
		try {
			jsonlib = Util.getRawString(getResources(), R.raw.json2);
		} catch(Exception e) {
			Log.e(TAG, "Problem loading raw json library", e);
		}
		
		script = String.format("javascript:(function() { %s %s })();", jsonlib, script);
		
		script = script.replace(MATCH, String.format("%s'%d',", MATCH, magickey));
		script = script.replace(MATCH_RESULT, String.format("%s'%d',", MATCH_RESULT, magickey));
		
		return script;
		
	}
	
	
	/**
	 * Javascript bridge to help launch intents and return results. Any callers
	 * will need to provide the "magic key" to help protect against intent calls
	 * from non-injected code.
	 * 
	 * @author jsharkey
	 */
	final class IntentHelper {
		
		/**
		 * Resolve Intent constants, like Intent.ACTION_PICK
		 */
		private String getConstant(String key) {
			try {
				key = (String) Intent.class.getField(key).get(null);
			} catch(Exception e) {
			}
			return key;
		}
		
		/**
		 * Parse the given JSON string into an Intent. This would be a good
		 * place to add security checks in the future.
		 */
		private Intent parse(String jsonraw) {
			Intent intent = new Intent();
			
			Log.d(TAG, String.format("parse(jsonraw=%s)", jsonraw));
			
			try {
				JSONObject json = new JSONObject(jsonraw);
				
				// look for specific known variables, otherwise assume extras
				// {"action":"ACTION_PICK","category":["CATEGORY_DEFAULT"],"type":"image/*"}

				Iterator keys = json.keys();
				while(keys.hasNext()) {
					String key = (String)keys.next();
					
					if("action".equals(key)) {
						intent.setAction(getConstant(json.optString(key)));
					} else if("category".equals(key)) {
						JSONArray categ = json.optJSONArray(key);
						for(int i = 0; i < categ.length(); i++)
							intent.addCategory(getConstant(categ.optString(i)));
					} else if("type".equals(key)) {
						intent.setType(json.optString(key));
					} else if("data".equals(key)) {
						intent.setData(Uri.parse(json.optString(key)));
					} else if("class".equals(key)) {
						intent.setClassName(BrowserActivity.this, json.optString(key));
					} else {
						// first try parsing extra as number, otherwise fallback to string
						Object obj = json.get(key);
						if(obj instanceof Integer)
							intent.putExtra(getConstant(key), json.optInt(key));
						else if(obj instanceof Double)
							intent.putExtra(getConstant(key), (float)json.optDouble(key));
						else
							intent.putExtra(getConstant(key), json.optString(key));
					}
					
				}
				
			} catch(Exception e) {
				Log.e(TAG, "Problem while parsing JSON into Intent", e);
				intent = null;
			}
			
			return intent;
		}
		
		/**
		 * Launch the intent described by JSON. Will only launch if magic key
		 * matches for this browser instance.
		 */
		public void startActivity(String trykey, String json) {
			if(magickey != Long.parseLong(trykey)) {
				Log.e(TAG, "Magic key from caller doesn't match, so we might have a malicious caller.");
				return;
			}

			Intent intent = parse(json);
			if(intent == null) return;
			
			try {
				BrowserActivity.this.startActivity(intent);
			} catch(ActivityNotFoundException e) {
				Log.e(TAG, "Couldn't find activity to handle the requested intent", e);
				Toast.makeText(BrowserActivity.this, R.string.browse_nointent, Toast.LENGTH_SHORT).show();
			}

		}

		/**
		 * Launch the intent described by JSON and block until result is
		 * returned. Will package and return the result as a JSON string. Will
		 * only launch if the magic key matches for this browser instance.
		 */
		public String startActivityForResult(String trykey, String json) {
			if(magickey != Long.parseLong(trykey)) {
				Log.e(TAG, "Magic key from caller doesn't match, so we might have a malicous caller.");
				return null;
			}
			
			Intent intent = parse(json);
			if(intent == null) return null;
			resultCode = Activity.RESULT_CANCELED;
			
			// start this intent and wait for result
			synchronized(this) {
				try {
					BrowserActivity.this.startActivityForResult(intent, 1);
					resultWait.acquire();
				} catch(ActivityNotFoundException e) {
					Log.e(TAG, "Couldn't find activity to handle the requested intent", e);
					Toast.makeText(BrowserActivity.this, R.string.browse_nointent, Toast.LENGTH_SHORT).show();
				} catch(Exception e) {
					Log.e(TAG, "Problem while waiting for activity result", e);
				}
			}
			
			JSONObject result = new JSONObject();
			result.optInt("resultCode", resultCode);
			
			// parse our response into json before handing back
			if(resultCode == Activity.RESULT_OK) {
				if(resultData.getExtras() != null) {
					try {
						JSONObject extras = new JSONObject();
						for(String key : resultData.getExtras().keySet())
							extras.put(key, resultData.getExtras().get(key));
						result.put("extras", extras);
						
					} catch (JSONException e1) {
						Log.e(TAG, "Problem while parsing extras", e1);
					}
				}
				
				if(resultData.getData() != null) {
					try {
						// assume that we are handling one contentresolver response
						Cursor cur = managedQuery(resultData.getData(), null, null, null, null);
						cur.moveToFirst();
						
						JSONObject data = new JSONObject();
						for(int i = 0; i < cur.getColumnCount(); i++)
							data.put(cur.getColumnName(i), cur.getString(i));
						result.put("data", data);
					
					} catch (Exception e) {
						Log.e(TAG, "Problem while parsing data result", e);
					}
				}
			}
			
			String resultraw = result.toString();
			Log.d(TAG, String.format("startActivityForResult() result=%s", resultraw));
			return resultraw;
			
		}

	}

	final class OilCanChrome extends WebChromeClient {
		
		public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
			new AlertDialog.Builder(BrowserActivity.this).setMessage(message)
				.setPositiveButton(android.R.string.ok, null).create().show();
			
			result.confirm();
			return true;
			
		}
		
		public void onProgressChanged(WebView view, int newProgress) {
	        BrowserActivity.this.setProgress(newProgress * 100);
		}
		
		public void onReceivedTitle(WebView view, String title) {
			BrowserActivity.this.setTitle(BrowserActivity.this.getString(R.string.browse_title, title));
		}

	};
	
	public final static String USERSCRIPT_EXTENSION = ".user.js";

	final class OilCanClient extends WebViewClient {
		
		/**
		 * Watch each newly loaded page for userscript extensions (.user.js) to
		 * prompt user with install helper.
		 */
		public void onPageStarted(WebView view, final String url, Bitmap favicon) {
			
			// if url matches userscript extension, launch installer helper dialog
			if(url.endsWith(BrowserActivity.USERSCRIPT_EXTENSION)) {
				new AlertDialog.Builder(BrowserActivity.this)
					.setTitle(R.string.install_title)
					.setMessage(getString(R.string.install_message, url))
					.setPositiveButton(android.R.string.ok, new OnClickListener() {
						public void onClick(DialogInterface dialog, int which) {
							try {
								String raw = Util.getUrlString(url);
								scriptdb.insertScript(null, raw);
								Toast.makeText(BrowserActivity.this, R.string.manage_import_success, Toast.LENGTH_SHORT).show();
							} catch(Exception e) {
								Log.e(TAG, "Problem while trying to import script", e);
								Toast.makeText(BrowserActivity.this, R.string.manage_import_fail, Toast.LENGTH_SHORT).show();
							}
						}
					}).setNegativeButton(android.R.string.cancel, null).create().show();
				
			}

		}
		
		/**
		 * Handle finished loading of each page. Specifically this checks for
		 * any active scripts based on the URL. When found a matching site, we
		 * inject the JSON library and the applicable script.
		 */
		public void onPageFinished(WebView view, String url) {
			if(scriptdb == null) {
				Log.e(TAG, "ScriptDatabase wasn't ready for finished page");
				return;
			}
			
			// for each finished page, try looking for active scripts
			List<String> active = scriptdb.getActive(url);
			Log.d(TAG, String.format("Found %d active scripts on url=%s", active.size(), url));
			if(active.size() == 0) return;
			
			// inject each applicable script into page
			for(String script : active) {
				script = BrowserActivity.this.prepareScript(script);
				webview.loadUrl(script);
				
			}
			
		}
		
		public boolean shouldOverrideUrlLoading(WebView view, String url) {
			return false;
		}
		
	}

}